const utils = require("./util.js")
var CRYPTO = require("./crypto/crypto.js")
var crypto = new CRYPTO();


const SAR = {
  complete_message: 0,
  first_message: 1,
  continue_message: 2,
  last_message: 3
}
const MessageType = {
  Network_PDU: 0,
  Mesh_Beacon: 1,
  Configuration: 2,
  Provisioning_PDU: 3
}
const ProvisioningPDUType = {
  ProvisioningInvite: 0,
  ProvisioningCapabilities: 1,
  ProvisioningStart: 2,
  ProvisioningPublicKey: 3,
  ProvisioningInputComplete: 4,
  ProvisioningConfirmation: 5,
  ProvisioningRandom: 6,
  ProvisioningData: 7,
  ProvisioningComplete: 8,
  ProvisioningFailed: 9
}
const BeaconType = {
  UnprovisionedDevicebeacon:0,
  SecureNetworkbeacon:1,
}

class ProvisioningContentPdu{
  constructor(provisioningPDUType){
    this.ProvisioningPDUType = provisioningPDUType;
  }
  toProxyPdu() {
    var header = this.ProvisioningPDUType & 0x3F;
    var content = utils.intToHex(header)+this.toPdu();
    return ProxyPdu.prototype.deriveProxyPdu(MessageType.Provisioning_PDU, content);
  }
  parse() {}
  toPdu() {return "";}
}

class Invite extends ProvisioningContentPdu{
  constructor(){
    super(ProvisioningPDUType.ProvisioningInvite);
    this.attentionDuration;
  }
  parse(inviteParameters) {
    this.attentionDuration = parseInt(inviteParameters, 16);
  }
  toPdu() {
    return utils.intToHex(this.attentionDuration);
  }
  derive(attentionDuration) {
    var invite = new Invite();
    invite.attentionDuration = attentionDuration;
    return invite;
  }
}


class Capabilities extends ProvisioningContentPdu{
  constructor(){
    super(ProvisioningPDUType.ProvisioningCapabilities);
    this.numberOfElements;
    this.algorithms;
    this.publicKeyType;
    this.staticOOBType;
    this.outputOOBSize;
    this.outputOOBAction;
    this.inputOOBSize;
    this.inputOOBAction;
  }
  parse(capabilitiesParameters) {
    this.numberOfElements = parseInt(capabilitiesParameters.substring(0, 2), 16);
    this.algorithms = parseInt(capabilitiesParameters.substring(2, 6), 16);
    this.publicKeyType = parseInt(capabilitiesParameters.substring(6, 8), 16);
    this.staticOOBType = parseInt(capabilitiesParameters.substring(8, 10), 16);
    this.outputOOBSize = parseInt(capabilitiesParameters.substring(10, 12), 16);
    this.outputOOBAction = parseInt(capabilitiesParameters.substring(12, 16), 16);
    this.inputOOBSize = parseInt(capabilitiesParameters.substring(16, 18), 16);
    this.inputOOBAction = parseInt(capabilitiesParameters.substring(18, 22), 16);
  }
  derive(numberOfElements, algorithms, publicKeyType, staticOOBType, outputOOBSize, outputOOBAction, inputOOBSize, inputOOBAction) {
    var capabilities = new Capabilities();
    capabilities.attentionDuration = attentionDuration;
    capabilities.algorithms = algorithms;
    capabilities.publicKeyType = publicKeyType;
    capabilities.staticOOBType = staticOOBType;
    capabilities.outputOOBSize = outputOOBSize;
    capabilities.outputOOBAction = outputOOBAction;
    capabilities.inputOOBSize = inputOOBSize;
    capabilities.inputOOBAction = inputOOBAction;
    return capabilities;
  }
  toPdu(){
    var pdu="";
    pdu += utils.toHex(this.numberOfElements,1);
    pdu += utils.toHex(this.algorithms,2);
    pdu += utils.toHex(this.publicKeyType,1);
    pdu += utils.toHex(this.staticOOBType,1);
    pdu += utils.toHex(this.outputOOBSize,1);
    pdu += utils.toHex(this.outputOOBAction,2);
    pdu += utils.toHex(this.inputOOBSize,1);
    pdu += utils.toHex(this.inputOOBAction,2);
    return pdu;
  }
}

class Start extends ProvisioningContentPdu{
  constructor(){
    super(ProvisioningPDUType.ProvisioningStart)
    this.algorithm;
    this.publicKey;
    this.authenticationMethod;
    this.authenticationAction;
    this.authenticationSize;
  }
  parse(startParameters) {
    this.algorithm = parseInt(startParameters.substring(0, 2), 16);
    this.publicKey = parseInt(startParameters.substring(2, 4), 16);
    this.authenticationMethod = parseInt(startParameters.substring(4, 6), 16);
    this.authenticationAction = parseInt(startParameters.substring(6, 8), 16);
    this.authenticationSize = parseInt(startParameters.substring(8, 10), 16);
  }
  toPdu() {
    var pdu = "";
    pdu += utils.intToHex(this.algorithm);
    pdu += utils.intToHex(this.publicKey);
    pdu += utils.intToHex(this.authenticationMethod);
    pdu += utils.intToHex(this.authenticationAction);
    pdu += utils.intToHex(this.authenticationSize);
    return pdu;
  }
  derive(algorithm , publicKey , authenticationMethod ,authenticationAction,authenticationSize ) {
    var start =  new Start();
    start.algorithm = algorithm;
    start.publicKey = publicKey;
    start.authenticationMethod = authenticationMethod;
    start.authenticationAction = authenticationAction;
    start.authenticationSize = authenticationSize;
    return start;
  }
}


class PublicKey extends ProvisioningContentPdu{
  constructor(){
    super(ProvisioningPDUType.ProvisioningPublicKey);
    this.publicKeyX;
    this.publicKeyY;
  }
  parse (publicKeyParameters) {
    this.publicKeyX = publicKeyParameters.substring(0,64);
    this.publicKeyY = publicKeyParameters.substring(64, 128);
  }
  toPdu() {
    var pdu = "";
    pdu += this.publicKeyX;
    pdu += this.publicKeyY;
    return pdu;
  }
  derive(publicKeyX, publicKeyY ) {
    var publicKey = new PublicKey();
    publicKey.publicKeyX = publicKeyX;
    publicKey.publicKeyY = publicKeyY;
    return publicKey;
  }
}

class InputComplete extends ProvisioningContentPdu{
  constructor(){
    super(ProvisioningPDUType.ProvisioningInputComplete);
  }
  derive() {
    var inputComplete = new InputComplete();
    return inputComplete;
  }
}

class Confirmation extends ProvisioningContentPdu{
  constructor(){
    super(ProvisioningPDUType.ProvisioningConfirmation);
    this.confirmation;
    this.confirmationSalt;
  }
  parse(confirmationParameters) {
    this.confirmation = confirmationParameters;
  }
  toPdu() {
    return this.confirmation;
  }
  derive(ECDHSecret, invitePDUValue, capabilitiesPDUValue, startPDUValue, PublicKeyProvisioner, PublicKeyDevice, randomProvisioner){
    var confirm=new Confirmation();
    var confirmationInputs = invitePDUValue + capabilitiesPDUValue + startPDUValue + PublicKeyProvisioner + PublicKeyDevice;
    var prck = utils.bytesToHex(utils.toAsciiCodes("prck"));
    this.confirmationSalt=crypto.s1(confirmationInputs);
    
    var confirmationKey = crypto.k1(ECDHSecret, this.confirmationSalt, prck);
    var authValue ="00000000000000000000000000000000";
    var confirmationProvisioner = crypto.getAesCmac(confirmationKey.toString(), randomProvisioner + authValue);
    confirm.confirmation = confirmationProvisioner.toString();
    return confirm;
  }
}

class Random extends ProvisioningContentPdu{
  constructor(){
    super(ProvisioningPDUType.ProvisioningRandom);
    this.random;
  }
  parse (randomParameters) {
    this.random = randomParameters;
  }
  derive(randomParameters) {
    var random=new Random();
    random.random=randomParameters;
    return random;
  }
  toPdu() {
    return this.random;
  }
}

class ProvisioningData extends ProvisioningContentPdu{
  constructor(){
    super(ProvisioningPDUType.ProvisioningData);
    this.encryptedProvisioningData;
    this.provisioningDataMIC;
    this.devKey;
  }
  parse(dataParameters) {
    this.encryptedProvisioningData = dataParameters.substring(0, 50);
    this.provisioningDataMIC = dataParameters.substring(50, 66);
  }
  derive(netKey, keyIndex, flags, iVIndex, unicastAddress, confirmationSalt, randomProvisioner, randomDevice, ECDHSecret) {
    var data = new ProvisioningData();
    var provisioningSalt = crypto.s1(confirmationSalt + randomProvisioner + randomDevice);
    var prsk = utils.bytesToHex(utils.toAsciiCodes("prsk"));
    var prsn = utils.bytesToHex(utils.toAsciiCodes("prsn"));
    var prdk = utils.bytesToHex(utils.toAsciiCodes("prdk"));
    data.devKey = crypto.k1(ECDHSecret, provisioningSalt, prdk).toString();
    var sessionKey = crypto.k1(ECDHSecret, provisioningSalt, prsk).toString();
    var sessionNonce = crypto.k1(ECDHSecret, provisioningSalt, prsn).toString();
    sessionNonce = sessionNonce.substring(sessionNonce.length - 26, sessionNonce.length);
    var provisioningData = netKey + keyIndex + flags + iVIndex + unicastAddress;
    var result=crypto.encryptedProvisioningData(sessionKey, sessionNonce, provisioningData)
    data.encryptedProvisioningData = result.encryptedProvisioningData;
    data.provisioningDataMIC = result.provisioningDataMIC;
    return data;
  }
  toPdu() {
    return this.encryptedProvisioningData + this.provisioningDataMIC;
  }
}

class Complete extends ProvisioningContentPdu{
  constructor() {
    super(ProvisioningPDUType.ProvisioningComplete);
  }
  derive() {
    var complete = new Complete();
    return complete;
  }
}

class Failed extends ProvisioningContentPdu{
  constructor(){
    super(ProvisioningPDUType.ProvisioningFailed);
    this.errorCode;
  }
  parse(failedParameters) {
    this.errorCode = failedParameters;
  }
  derive(errorCode) {
    var failed = new Failed();
    failed.errorCode = errorCode;
    return failed;
  }
  toPdu() {
    return this.errorCode;
  }
}

class ProvisioningPdu{
  constructor(){
    this.type;
    this.parameters;
  }
  parse (provisioningPDU) {
    var header = parseInt(provisioningPDU.substring(0, 2), 16);
    var parameters = provisioningPDU.substring(2, provisioningPDU.length);
    this.type = header & 0x3f;
    switch (this.type) {
      case ProvisioningPDUType.ProvisioningInvite:
        this.parameters = new Invite();
        this.parameters.parse(parameters);
        break;
      case ProvisioningPDUType.ProvisioningCapabilities:
        this.parameters = new Capabilities();
        this.parameters.parse(parameters);
        break;
      case ProvisioningPDUType.ProvisioningStart:
        this.parameters = new Start();
        this.parameters.parse(parameters);
        break;
      case ProvisioningPDUType.ProvisioningPublicKey:
        this.parameters = new PublicKey();
        this.parameters.parse(parameters);
        break;
      case ProvisioningPDUType.ProvisioningInputComplete:
        this.parameters = new InputComplete();
        this.parameters.parse(parameters);
        break;
      case ProvisioningPDUType.ProvisioningConfirmation:
        this.parameters = new Confirmation();
        this.parameters.parse(parameters);
        break;
      case ProvisioningPDUType.ProvisioningRandom:
        this.parameters = new Random();
        this.parameters.parse(parameters);
        break;
      case ProvisioningPDUType.ProvisioningData:
        this.parameters = new ProvisioningData();
        this.parameters.parse(parameters);
        break;
      case ProvisioningPDUType.ProvisioningComplete:
        this.parameters = new Complete();
        this.parameters.parse(parameters);
        break;
      case ProvisioningPDUType.ProvisioningFailed:
        this.parameters = new Failed();
        this.parameters.parse(parameters);
        break;
      default:
        console.log("not know message type");
        break;
    }
  }
  isFailed() {
    if (this.parameters instanceof Failed) return true;
    return false;
  }
}

class SecureNetworkbeacon{
  constructor(){
    this.KeyRefreshFlag;
    this.IVUpdateFlag;
    this.networkID;
    this.iVIndex ;
    this.beaconKey;
    this.authenticationValue;
  }
  parse(beacon){
    let flags = parseInt(beacon.substring(0,2));
    this.KeyRefreshFlag = flags&0x1;
    this.IVUpdateFlag = flags >> 1 & 0x1;
    this.networkID = beacon.substring(2, 18);
    this.iVIndex = beacon.substring(18, 26);
    this.authenticationValue = beacon.substring(26, 42);
  }
  toPdu(){
    var pdu = utils.intToHex(BeaconType.SecureNetworkbeacon);
    let flag = utils.intToHex(this.KeyRefreshFlag | this.IVUpdateFlag<<1);
    pdu += flag;
    pdu += this.networkID;
    pdu += this.iVIndex;
    this.authenticationValue = crypto.getAesCmac(this.beaconKey, flag + this.networkID + this.iVIndex).toString().substring(0,16);
    pdu += this.authenticationValue;
    return pdu;
  }
}
class Meshbeacons {
  constructor(){
    this.beaconType;
    this.beacon;
  }
  parse(beaconHexString){
    this.beaconType = parseInt(beaconHexString.substring(0, 2), 16);
    var beacon = beaconHexString.substring(2, beaconHexString.length);
    switch (this.beaconType) {
      case BeaconType.UnprovisionedDevicebeacon:
        break;
      case BeaconType.SecureNetworkbeacon:
        this.beacon=new SecureNetworkbeacon();
        this.beacon.parse(beacon);
        break;
      default:
        console.log("not know beacon type");
        break;
    }
  }
}


class ProxyPdu{
  constructor(){
    this.sar;
    this.messageType;
    this.pdu;
  }
  registerProvisioningListener(listen){
    ProxyPdu.prototype.provisioning = listen
  }
  registerBleCommandListener(listen) {
    ProxyPdu.prototype.bleCommand = listen
  }
  registerMeshBeaconListener(listen) {
    ProxyPdu.prototype.meshBeacon = listen
  }
  parse(proxyPduHexString) {
    var header = parseInt(proxyPduHexString.substring(0, 2), 16);
    var pduHexString = proxyPduHexString.substring(2, proxyPduHexString.length);
    var sar = (header >> 6) & 0x3;
    var messageType = header & 0x3f;
    switch (sar) {
      case SAR.complete_message:
        ProxyPdu.prototype.parsePdu(messageType, pduHexString);
        break;
      case SAR.first_message:
        ProxyPdu.prototype.pduHexString = pduHexString;
        break;
      case SAR.continue_message:
        ProxyPdu.prototype.pduHexString += pduHexString;
        break;
      case SAR.last_message:
        ProxyPdu.prototype.pduHexString += pduHexString;
        ProxyPdu.prototype.parsePdu(messageType, ProxyPdu.prototype.pduHexString);
        break;
    }
  }
  parsePdu(messageType,pduHexString){
    var pdu;
    switch (messageType) {
      case MessageType.Network_PDU: 
        if (ProxyPdu.prototype.bleCommand){
          ProxyPdu.prototype.bleCommand.receiveNetworkPdu(pduHexString)
        }
        break;
      case MessageType.Mesh_Beacon:
        pdu=new Meshbeacons();
        pdu.parse(pduHexString);
        if (pdu.beaconType == BeaconType.SecureNetworkbeacon&&ProxyPdu.prototype.meshBeacon){
          ProxyPdu.prototype.meshBeacon.receiveSecureNetworkbeacon(pdu);
        }
        break;
      case MessageType.Provisioning_PDU:
        pdu = new ProvisioningPdu();
        pdu.parse(pduHexString);
        if (ProxyPdu.prototype.provisioning){
          ProxyPdu.prototype.provisioning.receiveProvisioning(pdu);
        }
        break; 
      case MessageType.Proxy_Configuration:
        break;
      default:
        console.log("not know message type");
        break;
    }
  }
  deriveProxyPdu (msg_type, content) {
    var proxy_pdu=[];
    var proxy_pdu_size = 20;
    if (ProxyPdu.prototype.system.toLowerCase().indexOf("ios")!=-1){
      proxy_pdu_size = 512;
    }
    if (content.length < (proxy_pdu_size-1)*2){
      var sm = (SAR.complete_message << 6) | msg_type;
      proxy_pdu.push(utils.intToHex(sm) + content);
    }else{
      for (let i = 0; i < content.length; i = i + (proxy_pdu_size - 1) * 2){
        var sm;
        if(i==0){
          sm= (SAR.first_message << 6) | msg_type;
        } else if ((i + (proxy_pdu_size - 1) * 2 )>= content.length){
          sm = (SAR.last_message << 6) | msg_type;
        }else{
          sm = (SAR.continue_message << 6) | msg_type;
        }
        proxy_pdu.push(utils.intToHex(sm) + content.substring(i, i + (proxy_pdu_size - 1) * 2));
      }
    }
    return proxy_pdu;
  }

  deriveProvisioningInvite  ({attentionDuration=0}) {
    return Invite.prototype.derive(attentionDuration);
  }

  deriveProvisioningStart({algorithm=0,publicKey=0,authenticationMethod=0,authenticationAction=0,authenticationSize=0}){
    return Start.prototype.derive(algorithm, publicKey, authenticationMethod, authenticationAction, authenticationSize);
  }

  deriveProvisioningPublicKey({ publicKeyX, publicKeyY }) {
    return PublicKey.prototype.derive(publicKeyX, publicKeyY);
  }

  deriveProvisioningConfirmation(ECDHSecret, invitePDUValue, capabilitiesPDUValue, startPDUValue, 
  PublicKeyProvisioner, PublicKeyDevice, randomProvisioner) {
    return Confirmation.prototype.derive(ECDHSecret, invitePDUValue, capabilitiesPDUValue,
     startPDUValue,PublicKeyProvisioner,PublicKeyDevice, randomProvisioner);
  }

  deriveProvisioningRandom(random){
    return Random.prototype.derive(random);
  }

  deriveProvisioningData({ netKey, keyIndex, flags = "00", iVIndex, unicastAddress, confirmationSalt, randomProvisioner, randomDevice, ECDHSecret}) {
    return ProvisioningData.prototype.derive(netKey, keyIndex, flags, iVIndex, unicastAddress,confirmationSalt, randomProvisioner, randomDevice, ECDHSecret);
  }

  deriveNetworkPdu(networkPdu){
    return this.deriveProxyPdu(MessageType.Network_PDU, networkPdu);
  }

  deriveConfigurePdu(configurePdu) {
    return this.deriveProxyPdu(MessageType.Configuration, configurePdu);
  }

  deriveMeshBeaconPdu(meshBeaconPdu) {
    return this.deriveProxyPdu(MessageType.Mesh_Beacon, meshBeaconPdu);
  }

}
ProxyPdu.prototype.system = wx.getSystemInfoSync()["system"];

module.exports = {
  ProxyPdu: ProxyPdu,
  SecureNetworkbeacon: SecureNetworkbeacon,
}